繼上篇講過的模組標準百花齊放後,迎來了分久必合,到了 ES 6,ECMAScript 終於有了關於模組的標準定義,寫於 Module
章節裡。
當模組指稱 ES 6 的 Module 的時候,我們可能會簡寫為 ESM(ES 6 Module)。
在 ESM 裡面,引入和導出分別使用 import
和 export
兩個關鍵字。
ESM 把單一的檔案視為一個模組,作用域就鎖於該檔案內,因此引入後不會有全域污染的問題。
導出時使用 export
,對象可以是物件、函示、類別、變數。
另外導出時可以決定要匿名預設導出或具名導出。
// module.js
export function namedExport() {//具名導出
console.log('I am a named export');
}
export default function() {//匿名預設導出
console.log('I am the default export');
}
可以看到範例多了一個關鍵字 default
,該關鍵字能起到的作用是,透過 export default
導出的內容,引入時無需使用大括號命名去接,而是會直接接到該 default
對象。
同時,整個模組內只能有一個 default
,多於一個會直接報錯。(Identifier '.default' has already been declared"
)。
引入時使用 import
,加上大括號來對應導出對象的名字,除非是使用 export default
方式導出的;引入時模組來源可用相對路徑,但要包含完整檔名如 .js
(某些環境透過設定可能可以省略)。
以上面導出的例子而言,假設 module.js
是一個同路徑下的檔案,可以這樣寫:
import defaultExport, { namedExport } from './module.js';
// 使用 foo 和 bar 函數
namedExport(); // "I am a named export"
defaultExport(); // "I am the default export"
這個例子也示範了具名和預設匿名導出是可以混用的。
ESM 引入或導出的的時候,有一個關鍵字 as
可以用於重新命名本來檔案裡的函式或物件,如下面例子,本來原檔案裡匯出的函式是 function1 和 function2,但 as
讓函式在外面被引入時改為使用 newFunctionName 和 anotherNewFunctionName 來引入。
//module.js
export { function1 as newFunctionName, function2 as anotherNewFunctionName };
//main.js
import {newFunctionName, anotherNewFunctionName} from './module.js';
//as 寫在 import 的寫法
export { function1 , function2};
import { function1 as newFunctionName, function2 as anotherNewFunctionName} from './module.js';
重命名讓我們在引入模組的時候能避免名稱衝突,也可以使用更貼近該檔案風格的命名。
瀏覽器中的 ESM 的模組載入時間是屬於延遲載入,有點類似過往的語法中在 <script>
上加上 defer
的感覺,不會阻塞頁面的渲染,會等到 HTML 解析完成後,在 Load
事件觸發前先載入。
import
的語法屬同步載入,會待載入完才繼續往下執行。
但若要動態載入, ESM 也有提供 import()
的寫法,使撰寫時更為靈活。
import("customModule.js").then((module) => {
// Do something with the module.
});
這樣的寫法 import()
會返回一個 Promise
物件,可以用對應的 .then()
的寫法去做後續載入模組的處理。
當使用到 export
和 import
時有以下幾點需要注意:
export
和 import
兩個關鍵字都只能在各個檔案內的頂層作用域中被使用,否則會無法識別而報錯。import
或 export
,整個檔案就會自動變為嚴格模式("use strict"
),有用到的時候要注意該檔案有沒有用到任何會在嚴格模式出錯的語法。export
或 import
,載入需有 type="module"
的屬性。預設的屬性是 type="application/javascript"
撰寫模組和程式的時候,包含整理依賴與轉換成適合執行的方式,有個打包的概念:指經過編譯工具,產出最後實際執行的 JS 檔。
如以往在 CJS 的時候,有 Browserify 來打包 CJS 提供給前端使用。
後來 Webpack 被推出了,作為更新更靈活的 JS 模組打包工具,旨在提供簡化優化模組的管理,包含前後端都能使用。
相較於 Browserify,Webpack 可適用於 CJS,AMD,CMD,UMD,ESM,甚至除了模組外,還能夠打包 CSS 和圖片格式(給前端使用)。
Webpack 有個優化打包的概念: Tree Shaking
(中文有人會說樹搖,但使用不廣泛,大部分會直接說英文)。
具象化英文翻譯就是一個搖動樹的概念,把樹上的枯葉抖下來。這個概念被具象化到打包中,即指透過靜態分析的優勢,把沒有用到的程式碼在編譯的時期移除,減少最後文件打包的大小。
.mjs
和 .js
看別人的程式碼可能會看到有些檔案的副檔名是 .mjs
,如 V8引擎 推薦了這種做法。
好處是可以清楚看出哪些是作為模組作用,哪些是一般的 JS,某些運行環境可能也要求這種方式來便是模組。
但在現今的環境中,可能仍有瀏覽器/作業系統/伺服器沒辦法正確識別的這種副檔名。
就伺服器而言,對於各個文件而言會有一個 Content-Type
,需要指定文件的 MIME 類型,最好是用 text/javascript
來標示,這是 HTML standard 裡推薦的標示方法。
像 MDN 可能還有提到包含 javascript/esm
或 application/javascript
,但如上所說,於標準中被推薦的寫法是 type="text/javascript"
。
對於瀏覽器而言,如果是 <script>
標籤的引入方法需要加註 type="module"
,否則無法正確被識別 import
和 export
語法。
<script type="module"> //include script here </script>
講完了 JavaScript 中常見的五種模組標準(CJS、AMD、CMD、UMD、ESM),我們來最後做一個表格比較。
標準 | 模組載入時間 | 導出與引入 | 主要使用環境 | 使用場景 |
---|---|---|---|---|
CJS | 同步載入 | require / module.exports |
Node.js | 簡單易用,適合後端模組,不適合前端瀏覽器環境,無法異步加載。 |
AMD | 異步載入 | define / require |
瀏覽器 | 異步載入,適合前端。 |
CMD | 異步載入 | define / require |
瀏覽器 | 按需加載模組,依賴可以延遲執行。 |
UMD | 依據實際應用的方式 | 支持多種格式 (CJS, AMD) | 瀏覽器與 Node.js | 通用性強,適合跨平台應用,但程式碼會多一段要處理相容部分。 |
ESM | 提供同步載入和異步載入語法 | import / export |
現代瀏覽器和 Node.js | 語法簡潔,支持靜態分析和 Tree Shaking |
在現今的開發環境中,ESM 已是主流的寫法,透過整合的規範,只要是現代的瀏覽器,幾乎都會使用 ESM。當然過往的專案可能仍有使用其他載入方法的,希望這兩篇能幫助大家更全面的了解 JS 中的模組概念。